/*
 * Copyright (C) 2002-2006 the xine-project
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 *
 * $Id: playlist.c,v 1.175 2006/04/08 21:34:50 dsalt Exp $
 *
 * playlist implementation
 *
 * .pls parser stolen from totem (C) 2002 Bastien Nocera
 */

#include "globals.h"

#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

#include <X11/Xlib.h>

#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <glib.h>
#include <xine/xmlparser.h>

#include <pthread.h>

#include "playlist.h"
#include "http.h"
#include "ui.h"
#include "utils.h"
#include "menu.h"
#include "play_item.h"
#include "mediamarks.h"
#include "engine.h"
#include "player.h"
#include "drag_drop.h"

/*
#define LOG
*/

static int             is_visible;
static GtkUIManager   *pl_ui;
static GtkListStore   *pl_store;
static GtkWidget      *dlg;
static GtkTreeView    *tree_view;
static GtkTreeSelection *sel;
static GtkToggleButton *repeat_button, *random_button;
static char            logo_mrl[1024];
static int             logo_mode;

static play_item_t    *cur_item = NULL;
static int             cur_list_pos;
static int	       ref_list_pos;

static pthread_mutex_t cur_item_lock = PTHREAD_MUTEX_INITIALIZER;
#define CUR_ITEM_LOCK()   pthread_mutex_lock (&cur_item_lock)
#define CUR_ITEM_UNLOCK() pthread_mutex_unlock (&cur_item_lock)

#define unmark_play_item() set_mark_play_item (FALSE)
#define mark_play_item()   set_mark_play_item (TRUE)
static void set_mark_play_item (gboolean state);
static void play_next (void);
static int playlist_clip (int);

#define COLUMN_TITLE		0
#define COLUMN_MRL		1
#define COLUMN_PLAY_ITEM	2
#define COLUMN_MARKING		3
#define COLUMN_SOURCE		4

static play_item_t *peek_play_item (GtkTreeIter *iter)
{
  GValue v = {};
  gtk_tree_model_get_value (GTK_TREE_MODEL (pl_store), iter,
			    COLUMN_PLAY_ITEM, &v);
  void *p = g_value_peek_pointer (&v);
  g_value_unset (&v);
  return p;
}

static inline void remove_trailing_cr (char *str)
{
  str = strrchr (str, '\r');
  if (str && str[1] == 0)
    *str = 0;
}

static inline void cur_item_dispose (void)
{
  CUR_ITEM_LOCK ();
  if (cur_item)
  {
    play_item_dispose (cur_item);
    cur_item = NULL;
  }
  CUR_ITEM_UNLOCK ();
}

static gboolean item_marked_current (GtkTreeIter *iter)
{
  GValue v = {};
  gtk_tree_model_get_value (GTK_TREE_MODEL (pl_store), iter,
			    COLUMN_MARKING, &v);
  const char *p = g_value_peek_pointer (&v);
  gboolean ret = p && *p;
  g_value_unset (&v);
  return ret;
}

static gboolean item_is_normal (GtkTreeIter *iter)
{
  play_item_t *item = peek_play_item (iter);
  return item->type == PLAY_ITEM_NORMAL;
}

static gboolean playlist_browse_set (const play_item_t *item)
{
  playlist_flush (PLAY_ITEM_BROWSER);
  if (!item)
    return FALSE;

  const char *const *plugins = xine_get_browsable_input_plugin_ids (xine);
  if (!plugins)
    return FALSE;

  int idlen = (strchr (item->mrl, ':') ? : item->mrl) - item->mrl;
  int i, count;

  if (!strncasecmp ("file", item->mrl, idlen)) /* don't do 'file:' MRLs */
    return FALSE;

  for (i = 0; plugins[i]; ++i)
    if (!plugins[i][idlen] && !strncasecmp (plugins[i], item->mrl, idlen))
      break;
  if (!plugins[i]) /* end of list => plugin isn't browsable */
    return FALSE;

  xine_mrl_t **mrls = xine_get_browse_mrls (xine, plugins[i], item->mrl, &count);
  if (!mrls || !count) /* no browse MRLs */
    return FALSE;

  for (i = 0; i < count; ++i)
  {
    play_item_t *item = play_item_new (NULL, mrls[i]->mrl, 0);
    item->type = PLAY_ITEM_BROWSER;
    playlist_add (item, -1);
  }

  return FALSE;
}

static gboolean close_cb (GtkWidget* widget, gpointer data)
{
  is_visible = FALSE;
  gtk_widget_hide (dlg);

  return TRUE;
}

int playlist_flush (play_item_type type)
{
  GtkTreeIter iter;
  int count = 0;

  restart:
  if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (pl_store), &iter))
    goto done;

  int pos = 0;
  do
  {
    if (peek_play_item (&iter)->type == type)
    {
      if (pos == cur_list_pos && !logo_mode)
	se_eval (gse, "stop()", NULL, NULL, NULL, NULL);
      if (pos <= cur_list_pos)
	--cur_list_pos;
      gtk_list_store_remove (pl_store, &iter);
      ++count;
      goto restart;
    }

    ++pos;
  } while (gtk_tree_model_iter_next (GTK_TREE_MODEL(pl_store), &iter));

  done:
  return count;
}

void playlist_clear (void)
{
  if (!logo_mode)
    se_eval (gse, "stop()", NULL, NULL, NULL, NULL);
  if (!playlist_flush (PLAY_ITEM_NORMAL))
    gtk_list_store_clear (pl_store);
}

static void clear_cb (GtkWidget* widget, gpointer data)
{
  playlist_clear ();
  playlist_logo (NULL);
}

static void make_mediamark_cb (GtkWidget* widget, gpointer data)
{
  GtkTreeIter iter;

  if (gtk_tree_selection_get_selected (sel, NULL, &iter))
    mm_add (play_item_copy (peek_play_item (&iter)));
}

static void del_cb (GtkWidget* widget, gpointer data)
{
  GtkTreeIter iter;

  if (gtk_tree_selection_get_selected (sel, NULL, &iter))
  {
    gint        *indices;
    GtkTreePath *path;
    gint         pos;

    path = gtk_tree_model_get_path (GTK_TREE_MODEL (pl_store), &iter);
    indices = gtk_tree_path_get_indices (path);
    pos = indices[0];

    if (pos == cur_list_pos && !logo_mode)
      play_next ();

    if (pos <= cur_list_pos)
      --cur_list_pos;
    gtk_list_store_remove (pl_store, &iter);

    gtk_tree_selection_select_path (sel, path);
    gtk_tree_path_free (path);
  }
}

static void copy_cb (GtkWidget* widget, gpointer data)
{
  clip_set_play_item_from_selection ((GtkWidget *)tree_view);
}

static void cut_cb (GtkWidget* widget, gpointer data)
{
  copy_cb (widget, data);
  del_cb (widget, data);
}

static void paste_cb (GtkWidget* widget, gpointer data)
{
  play_item_t *item = clip_get_play_item ();
  if (item)
  {
    GtkTreeIter iter;

    if (gtk_tree_selection_get_selected (sel, NULL, &iter))
    {
      GtkTreePath *path;
      path = gtk_tree_model_get_path (GTK_TREE_MODEL (pl_store), &iter);
      playlist_add (item, gtk_tree_path_get_indices (path)[0] + 1);
      gtk_tree_path_free (path);
    }
    else
      playlist_add (item, -1);
  }
}

static void new_cb (GtkWidget* widget, gpointer data)
{
  play_item_t *play_item = play_item_new ("", "", 0);
  if (play_item_edit (play_item, PLAY_ITEM_LIST, NULL, dlg))
    playlist_add (play_item, -1);
  else
    play_item_dispose (play_item);
}

void playlist_add_list (GSList *fnames, gboolean play)
{
  if (fnames)
  {
    GSList *iter = fnames;
    int pos = playlist_add_mrl (iter->data, -1);

    foreach_glist (iter, iter->next)
    {
      playlist_add_mrl (iter->data, -1);
      free (iter->data);
    }
    g_slist_free (fnames);

    if (play)
      playlist_play (pos);
  }
}

static void add_cb (GtkWidget* widget, gpointer data)
{
  playlist_add_list
    (modal_multi_file_dialog (_("Select files to add to playlist"),
			      FALSE, NULL, dlg),
     FALSE);
}

static void edit_cb (GtkWidget* widget, gpointer data)
{
  GtkTreeIter iter;

  if (gtk_tree_selection_get_selected (sel, NULL, &iter))
  {
    play_item_t *play_item, *item_copy;
    const char  *title;

    play_item = peek_play_item (&iter);
    item_copy = play_item_copy (play_item); /* in case of edit */

    title = item_marked_current (&iter)
	    ? xine_get_meta_info (stream, XINE_META_INFO_TITLE) : NULL;

    if (play_item_edit (play_item, PLAY_ITEM_LIST, title, dlg))
    {
      if (item_marked_current (&iter))
      {
	CUR_ITEM_LOCK ();
	cur_item = item_copy;
	CUR_ITEM_UNLOCK ();
      }
      else
	play_item_dispose (item_copy);

      gtk_list_store_set (pl_store, &iter,
			  COLUMN_TITLE, play_item->title,
			  COLUMN_MRL, play_item->mrl,
			  COLUMN_PLAY_ITEM, play_item,
			  -1);
    }
    else
      play_item_dispose (item_copy);
  }
}

static void playlist_next_cb (GtkWidget *widget, gpointer data)
{
  int pos = playlist_get_list_pos ();
  int newpos = playlist_clip (pos + 1);
  if (newpos != pos || logo_mode)
    playlist_play (newpos);
}

static void playlist_prev_cb (GtkWidget *widget, gpointer data)
{
  int pos = playlist_get_list_pos ();
  int newpos = playlist_clip (pos - 1);
  if (newpos != pos || logo_mode)
    playlist_play (newpos);
}

static int playlist_load (const char *fname)
{
  xml_node_t *root = NULL;
  gboolean defaultfile = !fname;
  int ret = 0;

  if (!fname)
    fname = get_config_filename (FILE_PLAYLIST);

  struct stat st;
  char *plfile = NULL;
  if (!stat (fname, &st))
  {
    if (S_ISDIR (st.st_mode))
      errno = EISDIR;
    else if (!S_ISREG (st.st_mode))
      errno = ENOENT;
    else if (st.st_size > 4*1024*1024)
      errno = EFBIG;
    else
      plfile = read_entire_file (fname, NULL);
  }

  if (!plfile)
  {
    if (errno != ENOENT && (defaultfile || errno != ENOTDIR))
      display_error (FROM_GXINE, _("Loading of playlist file failed."),
		     _("Failed to open file ‘%s’\n%s"), fname, strerror (errno));
    goto ret0;
  }

  xml_parser_init (plfile, strlen (plfile), XML_PARSER_CASE_INSENSITIVE);

  if (xml_parser_build_tree (&root) < 0)
  {
    display_error (FROM_GXINE, _("Loading of playlist file failed."),
		   _("‘%s’ is not a valid XML/ASX file"), fname);
    goto ret0;
  }

  if (!strcasecmp (root->name, "ASX"))
  {
    /* Attributes: VERSION, PREVIEWMODE, BANNERBAR
     * Child elements: ABSTRACT, AUTHOR, BASE, COPYRIGHT, DURATION, ENTRY,
     ENTRYREF, MOREINFO, PARAM, REPEAT, TITLE
     */
    //const char *base_href = NULL;

    const char *version = xml_parser_get_property (root, "VERSION") ? : "";
    int version_major, version_minor = 0;
    xml_node_t *node;

    if (!((sscanf (version, "%d.%d", &version_major, &version_minor) == 2 ||
	   sscanf (version, "%d", &version_major) == 1) &&
	  (version_major == 3 && version_minor == 0)))
    {
      display_error (FROM_GXINE, _("Loading of playlist file failed."),
		     _("Unknown or incorrect ASX version number in ‘%s’"),
		     fname);
      goto ret0;
    }

    playlist_clear ();

    foreach_glist (node, root->child)
    {
      //const char *ref_base_href = base_href;

      if (!strcasecmp (node->name, "SETTINGS"))
      {
        /* gxine-specific */
	gtk_toggle_button_set_active
	  (random_button, xml_parser_get_property_bool (node, "RANDOM", 0));
	gtk_toggle_button_set_active
	  (repeat_button, xml_parser_get_property_bool (node, "REPEAT", 0));
      }

      else if (!strcasecmp (node->name, "ENTRY"))
      {
	play_item_t *play_item = play_item_load (node->child);
	GtkTreeIter  iter;
	gtk_list_store_append (pl_store, &iter);
	gtk_list_store_set (pl_store, &iter,
			    COLUMN_TITLE, play_item->title,
			    COLUMN_MRL, play_item->mrl,
			    COLUMN_PLAY_ITEM, play_item,
			    COLUMN_MARKING, "",
			    COLUMN_SOURCE, "",
			    -1);
      }

      else if (!strcasecmp (node->name, "ENTRYREF"))
      {
	/* Attributes: HREF, CLIENTBIND */
	play_item_t *play_item =
	  play_item_new (NULL, xml_parser_get_property (node, "HREF"), 0);
	GtkTreeIter  iter;
	gtk_list_store_append (pl_store, &iter);
	gtk_list_store_set (pl_store, &iter,
			    COLUMN_TITLE, play_item->title,
			    COLUMN_MRL, play_item->mrl,
			    COLUMN_PLAY_ITEM, play_item,
			    COLUMN_MARKING, "",
			    COLUMN_SOURCE, "",
			    -1);
      }
/*
      else if (!strcasecmp (node->name, "BASE"))
	base_href = xml_parser_get_property (node, "HREF");
*/
    }
  }
  else
  {
    display_error (FROM_GXINE, _("Loading of playlist file failed."),
		   _("‘%s’ is not an ASX file"), fname);
    goto ret0;
  }

  ret = 1;
  return 1;

  ret0:
  free (plfile);
  xml_parser_free_tree (root);
  if (defaultfile)
    free ((char *)fname);
  return ret;
}

#ifdef PARSE_PLAYLISTS
static int playlist_load_any (const char *);
#else
#define playlist_load_any(FNAME) playlist_load (FNAME)
#endif

void playlist_save (char *file)
{
  char *fname = file ? : get_config_filename (FILE_PLAYLIST);
  FILE *f = open_write (fname, _("Failed to save playlist"));

  if (f)
  {
    static const char tf[][8] = { "false", "true" };

    GtkTreeIter iter;

    fprintf
      (f, "<ASX VERSION=\"3.0\">\n"
          "  <SETTINGS REPEAT=\"%s\" RANDOM=\"%s\"/>\n",
       tf[!!gtk_toggle_button_get_active (repeat_button)],
       tf[!!gtk_toggle_button_get_active (random_button)]);

    if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (pl_store), &iter))
    {
      do
      {
	play_item_t *item = peek_play_item (&iter);
	if (item->type == PLAY_ITEM_NORMAL)
	{
	  char *xml = play_item_xml (item, 1);
	  fputs (xml, f);
	  free (xml);
	}
      } while (gtk_tree_model_iter_next (GTK_TREE_MODEL(pl_store), &iter));
    }

    fprintf (f, "</ASX>\n");
    close_write (fname, f, _("Failed to save playlist"));
  }

  if (fname != file)
    free (fname);
}

static void open_cb (GtkWidget* widget, gpointer data)
{
  char *filename = modal_file_dialog (_("Select playlist..."), TRUE, TRUE,
				      FALSE, "*.asx", NULL, dlg);
  if (filename)
    playlist_load_any (filename);
  free (filename);
}

static void save_as_cb (GtkWidget* widget, gpointer data)
{
  char *filename = modal_file_dialog (_("Save playlist as..."), FALSE, TRUE,
				      FALSE, "*.asx", FALSE, dlg);
  if (filename)
    playlist_save (filename);
  free (filename);
}

static int playlist_clip (int pos)
{
  int items = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (pl_store), NULL);
  return (pos == -1) ? (items - (items != 0))
		     : (pos < 0 || pos >= items) ? 0 : pos;
}

play_item_t *playlist_get_item (int pos)
{
  GtkTreeIter        iter;

  if ((pos == -2 /* highlighted item or first item */
       && gtk_tree_selection_get_selected (sel, NULL, &iter))
      ||
      ((pos = playlist_clip (pos)) >= 0 /* nth item */
       && gtk_tree_model_iter_nth_child
	    (GTK_TREE_MODEL (pl_store), &iter, NULL, pos)))
    return peek_play_item (&iter);
  else
    return NULL;
}

int playlist_get_list_pos (void)
{
  return cur_list_pos;
}

play_item_t *playlist_get_current_item (void)
{
  play_item_t *i;
  CUR_ITEM_LOCK ();
  i = play_item_copy (cur_item ? : playlist_get_item (cur_list_pos));
  CUR_ITEM_UNLOCK ();
  return i;
}

int playlist_size (void)
{
  return gtk_tree_model_iter_n_children (GTK_TREE_MODEL (pl_store), NULL);
}

int playlist_add (play_item_t *play_item, gint ins_pos)
{
  static const char *const playlist_icon[] = {
    [PLAY_ITEM_NORMAL] = "",
    [PLAY_ITEM_AUTOPLAY] = GTK_STOCK_JUMP_TO,
    [PLAY_ITEM_BROWSER] = GTK_STOCK_FIND,
    [PLAY_ITEM_PLAYLIST] = GTK_STOCK_MEDIA_PLAY,
  };
  GtkTreeIter  iter;
  GtkTreePath *path;
  gint        *indices;
  gint         ret;

  if (ins_pos<0)
    gtk_list_store_append (pl_store, &iter);
  else
    gtk_list_store_insert (pl_store, &iter, ins_pos);

  gtk_list_store_set (pl_store, &iter,
		      COLUMN_TITLE, play_item->title,
		      COLUMN_MRL, play_item->mrl,
		      COLUMN_PLAY_ITEM, play_item,
		      COLUMN_MARKING, "",
		      COLUMN_SOURCE, playlist_icon[play_item->type],
		      -1);

  /*
   * find out entry number in list
   */

  path = gtk_tree_model_get_path (GTK_TREE_MODEL (pl_store), &iter);
  indices = gtk_tree_path_get_indices (path);
  ret = indices[0];
  gtk_tree_path_free (path);

  return ret;
}

static inline void play_item_check_autoplay (play_item_t *item)
{
  int i, j;
  const char *const *autoplay_ids = xine_get_autoplay_input_plugin_ids (xine);
  if (autoplay_ids)
    for (i = 0; autoplay_ids[i]; ++i)
    {
      char **mrls = xine_get_autoplay_mrls (xine, autoplay_ids[i], &j);
      if (mrls)
	for (j = 0; mrls[j]; ++j)
	  if (!strcmp (mrls[j], item->mrl))
	  {
	    item->type = PLAY_ITEM_AUTOPLAY;
	    return;
	  }
    }
}

static gint playlist_add_int (const char *mrl, gint ins_pos)
{
  play_item_t *play_item;
  int i;

  /* don't add duplicate items */
  if (ins_pos < 0)
    for (i = 0; i < playlist_size (); ++i)
      if (!strcmp (playlist_get_item (i)->mrl, mrl))
        return i;

  play_item = play_item_new (NULL, mrl, 0);
  play_item_check_autoplay (play_item);

  return playlist_add (play_item, ins_pos);
}

#ifdef PARSE_PLAYLISTS

/* INI file parsing */

static int
read_ini_line_int (const char *const *lines, const char *key)
{
  int retval = -1;
  int i;

  if (lines == NULL || key == NULL)
    return -1;

  for (i = 0; (lines[i] != NULL && retval == -1); i++)
  {
    if (g_ascii_strncasecmp (lines[i], key, strlen (key)) == 0)
    {
      char **bits = g_strsplit (lines[i], "=", 2);
      if (bits[0] == NULL || bits [1] == NULL)
      {
	g_strfreev (bits);
	return -1;
      }

      retval = (gint) g_strtod (bits[1], NULL);
      g_strfreev (bits);
    }
  }

  return retval;
}

static char *
read_ini_line_string (const char *const *lines, const char *key)
{
  char *retval = NULL;
  int i;

  if (lines == NULL || key == NULL)
    return NULL;

  for (i = 0; (lines[i] != NULL && retval == NULL); i++)
  {
    if (g_ascii_strncasecmp (lines[i], key, strlen (key)) == 0)
    {
      char **bits = g_strsplit (lines[i], "=", 2);
      if (bits[0] == NULL || bits [1] == NULL)
      {
	g_strfreev (bits);
	return NULL;
      }

      retval = g_strdup (bits[1]);
      g_strfreev (bits);
    }
  }

  return retval;
}

/* Playlist loader framework */

typedef struct {
  xml_node_t *node;
  GPtrArray *parents;
  guint count;
  int data;
} pl_xml_info_t;

typedef struct {
  gboolean is_xml;
  char *(*download) (const char *, ssize_t *);
  union {
    struct {
      gboolean (*parse) (const char *const *, void **);
      play_item_t *(*fetch) (const char *const *, size_t, void *);
    } text;
    struct {
      gboolean (*parse) (pl_xml_info_t *, void **);
      play_item_t *(*fetch) (pl_xml_info_t *, void *);
      const char *root;
    } xml;
  };
  void (*cleanup) (void *);
} playlist_loader_t;

static inline void pl_push (pl_xml_info_t *info)
{
  if (info->node->next)
  {
    ++info->count;
    g_ptr_array_add (info->parents, info->node->next);
  }
  info->node = info->node->child;
}

static inline xml_node_t *pl_pop (pl_xml_info_t *info)
{
  return info->node = info->count
		    ? g_ptr_array_remove_index (info->parents, --info->count)
		    : NULL;
}
static int
playlist_load_generic (const char *mrl, gint ins_pos,
		       const playlist_loader_t *loader)
{
  int i, retval = -1;
  ssize_t size;
  char *contents = NULL, **lines = NULL;
  xml_node_t *xml_tree;
  pl_xml_info_t *xml_info = NULL;
  void *data = NULL;

  if (ins_pos == -2)
  {
    ins_pos = -1;
    playlist_clear ();
  }

  contents = loader->download ? loader->download (mrl, &size)
			      : read_entire_file (mrl, &size);
  if (!contents)
    return -1;

  if (loader->is_xml)
  {
    xml_parser_init (contents, size, XML_PARSER_CASE_INSENSITIVE);
    if (xml_parser_build_tree (&xml_tree))
    {
      g_free (contents);
      return -1;
    }
    if (loader->xml.root && strcmp (xml_tree->name, loader->xml.root))
    {
      g_printerr (_("playlist: no %s tag\n"), loader->xml.root);
      g_free (contents);
      return -1;
    }

    xml_info = malloc (sizeof (pl_xml_info_t));
    xml_info->node = NULL;
    xml_info->parents = g_ptr_array_new ();
    xml_info->data = 0;

    if ((xml_info->count = !!xml_tree->child))
      g_ptr_array_add (xml_info->parents, xml_tree->child);
  }
  else
  {
    lines = g_strsplit (contents, "\n", 0);
    for (i = 0; lines[i]; ++i)
      remove_trailing_cr (lines[i]);
  }
  g_free (contents);

  size_t index = 0;

  if (loader->is_xml
      ? (!loader->xml.parse || loader->xml.parse (xml_info, &data))
      : (!loader->text.parse || loader->text.parse ((const char *const *)lines,
						    &data)))
  {
    play_item_t *item;
    while ((item = loader->is_xml
		   ? loader->xml.fetch (xml_info, data)
		   : loader->text.fetch ((const char *const *)lines,
					 index, data)))
    {
      if (item == (play_item_t *)-1)
      {
	++index;
	continue;
      }

      if (ins_pos < 0)
        ins_pos = playlist_add (item, -1);
      else
        playlist_add (item, ins_pos);

      if (retval == -1)
        retval = ins_pos;

      ++ins_pos;
      ++index;
    }
  }

  if (loader->cleanup)
    loader->cleanup (data);

  if (loader->is_xml)
  {
    xml_parser_free_tree (xml_tree);
    free (xml_info);
  }
  else
    g_strfreev (lines);

  return retval;
}

/* Load SMIL playlists */

static play_item_t *
pl_smil_fetch (pl_xml_info_t *info, void *data)
{
  for (;;)
  {
    if (!info->node || !(info->node = info->node->next))
    {
      info->data = 0;
      if (!pl_pop (info))
	return NULL;
    }

    next:
    switch (info->data)
    {
    case 0:
      if (strcmp (info->node->name, "SEQ") &&
	  strcmp (info->node->name, "SWITCH"))
      {
	if (!info->node->child)
	  continue;
	pl_push (info);
	goto next;
      }
      if (!info->node->child)
	continue;
      info->data = 1 + (info->node->name[1] == 'W');
      pl_push (info);
      goto next;

    case 1: /* SEQ */
      if (!strcmp (info->node->name, "SWITCH"))
      {
	if (!info->node->child)
	  continue;
        info->data = 2;
        pl_push (info);
      }

    case 2: /* SWITCH */
      if (strcmp (info->node->name, "REF") &&
	  strcmp (info->node->name, "AUDIO") &&
	  strcmp (info->node->name, "IMG") &&
	  strcmp (info->node->name, "VIDEO"))
	goto next;

      char *mrl = xml_parser_get_property (info->node, "SRC");
      if (mrl)
	return play_item_new (NULL, mrl, 0);
    }
  }
}

static int
playlist_load_smil (const char *mrl, gint ins_pos)
{
  static const playlist_loader_t loader = {
    TRUE, NULL, { .xml = { NULL, pl_smil_fetch, "SMIL" } }, NULL
  };
  return playlist_load_generic (mrl, ins_pos, &loader);
}

/* Load Real* playlists */

static char *
pl_ram_download (const char *mrl, ssize_t *size_p)
{
  char buf[4];
  ssize_t size;

  if (!strncasecmp (mrl, "http://", 7))
    size = http_peek (mrl, buf, sizeof (buf), NULL, 0);
  else
  {
    int fd = open (mrl, O_RDONLY);
    if (fd < 0)
      return NULL;
    size = read (fd, buf, sizeof (buf));
    close (fd);
  }

  /* RealMedia file => fake a playlist entry */
  if (size == 4 &&
      buf[0] == '.' && buf[1] == 'R' && buf[2] == 'M' && buf[3] == 'F')
  {
    if (size_p)
      *size_p = strlen (mrl);
    return strdup (mrl);
  }

  return read_entire_file (mrl, size_p);
}

static play_item_t *
pl_ram_fetch (const char *const *lines, size_t index, void *data)
{
  /* ignore comments */
  if (lines[index][0] == '#')
    return (play_item_t *) -1;

  /* it's probably an RTSP or PNM MRL, but we also match HTTP here */
  if (!strncasecmp (lines[index], "rtsp://", 7)
      || !strncasecmp (lines[index], "pnm://", 6)
      || !strncasecmp (lines[index], "http://", 7))
    return play_item_new (NULL, lines[index], 0);

  return NULL;
}

static int
playlist_load_ram (const char *mrl, gint ins_pos)
{
  static const playlist_loader_t loader = {
    FALSE, pl_ram_download, { .text = { NULL, pl_ram_fetch } }, NULL
  };
  return playlist_load_generic (mrl, ins_pos, &loader);
}

/* Load M3U playlists */

static play_item_t *
pl_m3u_fetch (const char *const *lines, size_t index, void *data)
{
  /* Either it's a URI, or it has a proper path */
  if (!strstr (lines[index], "://") && lines[index][0] != G_DIR_SEPARATOR)
    return NULL;

  /* is this type parsing really needed? */
  const char *extension = strrchr (lines[index], '.');
  const char *suffix = (extension && !strncasecmp (extension, ".mp3", 4))
		       ? "#demux:mp3"
		       : (extension && !strncasecmp (extension, ".ogg", 4))
		       ? "#demux:ogg"
		       : NULL;
  char *tmp = g_strconcat (lines[index], suffix, NULL);
  play_item_t *item = play_item_new (NULL, tmp, 0);
  free (tmp);

  return item;
}

static int
playlist_load_m3u (const char *mrl, gint ins_pos)
{
  static const playlist_loader_t loader = {
    FALSE, NULL, { .text = { NULL, pl_m3u_fetch } }, NULL
  };
  return playlist_load_generic (mrl, ins_pos, &loader);
}

/* Load PLS playlists */

static char *
pl_pls_download (const char *mrl, ssize_t *size_p)
{
  /* http or local file ? */
  if (!strncmp (mrl, "http://", 7))
  {
    /* audio/mpeg => fake a playlist */
    char mime_type[256];
    http_peek (mrl, NULL, 0, mime_type, sizeof (mime_type));

    if (!strcmp(mime_type, "audio/mpeg"))
    {
      char *ret = g_strconcat ("[playlist]\nnumberofentries=1\nfile1=", mrl, NULL);
      if (size_p)
	*size_p = strlen (ret);
      return ret;
    }

    return http_download (mrl, size_p);
  }

  return read_entire_file (mrl, size_p);
}

static gboolean
pl_pls_parse (const char *const *lines, void **data)
{
  if (g_ascii_strncasecmp (lines[0], "[playlist]", 10))
    return FALSE;

  size_t *entries = *data = malloc (sizeof (size_t));
  *entries = read_ini_line_int (lines, "numberofentries");

  return TRUE;
}

static play_item_t *
pl_pls_fetch (const char *const *lines, size_t index, void *data)
{
  if (++index > *(unsigned int *)data)
    return NULL;

  char *key = g_strdup_printf ("file%zu", index);
  char *file = read_ini_line_string (lines, key);
  g_free (key);

  key = g_strdup_printf ("title%zu", index);
  char *title = read_ini_line_string (lines, key);
  g_free (key);

  if (file)
  {
    int pos = strlen (file);
    while (--pos > 0 && file[pos] < 32)
      file[pos] = 0;

    if (title)
    {
      pos = strlen (title);
      while (--pos > 0 && title[pos] < 32)
	title[pos] = 0;
    }

    play_item_t *ret = play_item_new (title, file, 0);
    g_free (file);

    return ret;
  }

  return NULL;
}

static int
playlist_load_pls (const char *mrl, gint ins_pos)
{
  static const playlist_loader_t loader = {
    FALSE, pl_pls_download, { .text = { pl_pls_parse, pl_pls_fetch } }, free
  };
  return playlist_load_generic (mrl, ins_pos, &loader);
}

/* Load ASX playlists */

static int playlist_load_asx (const char *mrl, gint ins_pos)
{
  char             *contents;
  char            **lines;
  ssize_t	    size;
  int		    res, ret, i;
  xml_node_t       *xml_tree;
  xml_node_t       *current_node_l1;
  xml_node_t       *current_node_l2;
  xml_property_t   *property;
  int               asx_version;

  if (ins_pos == -2)
    playlist_clear ();

  ret = -1;

  if (! (contents = read_entire_file (mrl, &size)))
    return ret;

  xml_parser_init (contents, size, XML_PARSER_CASE_INSENSITIVE);
  res = xml_parser_build_tree (&xml_tree);

  if (res)
  {
    /* humm, maybe the asx contains no xml? */
    /* try to find mms urls by hand */
    lines = g_strsplit (contents, "\n", 0);
    g_free (contents);

    for (i = 0; lines[i] != NULL; ++i)
    {
      /* look for mms:// in a line */
      if (strstr(lines[i], "mms://"))
      {
        remove_trailing_cr (lines[i]);
        /* add to playlist */
	if (ret >= 0)
          playlist_add_mrl (strstr(lines[i], "mms://"), ins_pos);
	else
	  ret = playlist_add_mrl (strstr(lines[i], "mms://"), ins_pos);
      }
    }
    return ret;
  }

  /* check ASX */
  if (strcmp(xml_tree->name, "ASX") == 0)
  {
    logprintf ("playlist: ASX tag detected\n");

    /* check version */
    asx_version = xml_parser_get_property_int (xml_tree, "VERSION", 3);

    if (asx_version == 3)
    {
      logprintf ("playlist: VERSION 3.0 detected\n");

      /* play each entry */
      foreach_glist (current_node_l1, xml_tree->child)
      {
	/* entry */
	if (strcmp (current_node_l1->name, "ENTRY") == 0)
	{
	  char *title = NULL;

	  logprintf("playlist: ENTRY detected\n");

	  /* play the first ref which is playable */
	  current_node_l2 = current_node_l1->child;
	  while (current_node_l2)
	  {
	    if (strcmp (current_node_l2->name, "REF") == 0)
	    {
	      logprintf ("playlist: REF detected\n");
	      /* find href property */
	      property = current_node_l2->props;
	      while (property && strcmp (property->name, "HREF"))
		property = property->next;

	      if (property)
	      {
		play_item_t *item;

		logprintf ("playlist: HREF property detected\n"
			   "playlist: found an mrl: %s\n", property->value);

		item = play_item_new (title, property->value, 0);

		if (ret >= 0)
		  playlist_add (item, ins_pos);
		else
		  ret = playlist_add (item, ins_pos);

		/* jump to next entry */
		current_node_l2 = NULL;

#if 0 /* FIXME */
		/* try to play REF */
		if (demux_asx_play(this, property->value) == 1)
		{
		  /* jump to next entry */
		  g_print ("playlist: play next entry or entryref\n");
		  current_node_l2 = NULL;
		}
		else /* try next REF */
		  g_print ("playlist: try next REF\n");
#endif

	      }
	      else /* !property */
		g_printerr (_("playlist: no HREF property\n"));

	    }
	    else if (strcmp(current_node_l2->name, "TITLE") == 0)
	      title = current_node_l2->data;
	    else
	      logprintf ("playlist: unknown tag %s detected\n",
			 current_node_l2->name);

	    if (current_node_l2)
	      current_node_l2 = current_node_l2->next;
	  } /* end while */
	}
	else
	{
	  /* entryref */
	  if (strcmp (current_node_l1->name, "ENTRYREF") == 0) {
	    logprintf ("playlist: ENTRYREF detected\n");
	    property = current_node_l1->props;
	    while (property && strcmp (property->name, "HREF"))
	      property = property->next;

	    if (property)
	    {
	      logprintf ("playlist: HREF property detected\n"
			 "playlist: found an MRL: %s\n", property->value);

	      if (ret >= 0)
		playlist_add_mrl (property->value, ins_pos);
	      else
		ret = playlist_add_mrl (property->value, ins_pos);

	      /* jump to next entry */
	      current_node_l2 = NULL;

#if 0 /* FIXME */

	      /* try to play HREF */
	      if (demux_asx_play(this, property->value) == 1)
	      {
		/* jump to next entry */
		g_print ("playlist: play next entry or entryref\n");
		current_node_l1 = NULL;
	      }
	      else /* try next REF */
		g_print("playlist: try next REF\n");
#endif
	    }
	    else /* !property */
	      g_printerr (_("playlist: no HREF property\n"));
	  }
	  else
	  {
	    /* title */
	    if (strcmp (current_node_l1->name, "TITLE") == 0)
	    {
	      logprintf ("playlist: TITLE detected\n");
	      /* change title */
	    }
	    else
	      g_printerr(_("playlist: unknown tag %s detected\n"),
		     current_node_l1->name);
	  }
	}
      }

    }
    else
      g_printerr (_("playlist: sorry, ASX version %d not implemented yet\n"),
		  asx_version);
  }
  else
    g_printerr(_("playlist: no ASX tag\n"));

  xml_parser_free_tree (xml_tree);
  g_free (contents);

  return ret;
}

static int playlist_add_playlist_mrl (const char *mrl, gint ins_pos)
{
  const char *extension = strrchr(mrl, '.');
  if (extension)
  {
    if (!strncasecmp (extension, ".pls", 4))
      return playlist_load_pls (mrl, ins_pos);

    if (!strncasecmp (extension, ".m3u", 4))
      return playlist_load_m3u (mrl, ins_pos);

    if (!strcasecmp (extension, ".ra") ||
	!strcasecmp (extension, ".rm") ||
	!strncasecmp (extension, ".ram", 4) ||
	!strncasecmp (extension, ".rpm", 4) ||
	!strncasecmp (extension, ".lsc", 4) ||
	!strncasecmp (extension, ".pl", 3))
      return playlist_load_ram (mrl, ins_pos);

    if (!strncasecmp (extension, ".asx", 4))
      return playlist_load_asx (mrl, ins_pos);

    if (!strncasecmp (extension, ".smi", 4))
      return playlist_load_smil (mrl, ins_pos);
  }

  return -2; /* not recognised */
}

static int playlist_load_any (const char *fname)
{
  const char *extension = strrchr(fname, '.');
  if (extension)
  {
    if (!strcasecmp (extension, ".pls"))
      return playlist_load_pls (fname, -2) < 0;

    if (!strcasecmp (extension, ".m3u"))
      return playlist_load_m3u (fname, -2) < 0;

    if (!strcasecmp (extension, ".ra") ||
	!strcasecmp (extension, ".rm") ||
	!strcasecmp (extension, ".ram") ||
	!strcasecmp (extension, ".rpm") ||
	!strcasecmp (extension, ".lsc") ||
	!strcasecmp (extension, ".pl"))
      return playlist_load_ram (fname, -2) < 0;

    if (!strcasecmp (extension, ".smi"))
      return playlist_load_smil (fname, -2) < 0;
  }

  return playlist_load (fname);
}

#endif /* PARSE_PLAYLISTS */

static int playlist_add_mrl_internal (const char *mrl, gint ins_pos)
{
  /* Helper function. Call only from playlist_add_mrl(). */
  int ret;
  char *uri = make_path_uri (mrl);

  if (uri) /* wasn't a URI - must be a local file */
  {
#ifdef PARSE_PLAYLISTS
    /* is it a playlist file? */
    ret = playlist_add_playlist_mrl (mrl, ins_pos);
    if (ret == -2)
#endif
    {
      logprintf ("playlist: adding regular mrl %s\n", uri);
      ret = playlist_add_int (uri, ins_pos);
    }
    free (uri);
    return ret;
  }

  if (!strncasecmp (mrl, "cdda:", 5))
    return playlist_add_int (mrl, ins_pos);

  if (!strncasecmp (mrl, "mmshttp://", 10))
  {
    logprintf ("playlist: adding regular mrl from mmshttp %s\n", mrl);
    mrl += 3;
    goto handle_http;
  }

  if (!strncasecmp (mrl, "http://", 7))
  {
    /* could still be a pls/asx playlist, but if so we
     * need to download it first
     */

    handle_http:
#ifdef PARSE_PLAYLISTS
    /* is it a playlist file? */
    ret = playlist_add_playlist_mrl (mrl, ins_pos);
    if (ret != -2)
      return ret;
#endif
    return playlist_add_int (mrl, ins_pos);
  }

  logprintf ("playlist: adding regular mrl %s\n", mrl);
  return playlist_add_int (mrl, ins_pos);
}

int playlist_add_mrl (const char *mrl, gint ins_pos /* -1 => append */)
{
  static GSList *queued = NULL;
  int ret;

  /* g_print (_("playlist_add called >%s<\n"), mrl); */

  if (g_slist_find_custom (queued, mrl, (GCompareFunc) strcmp))
  {
    /* oops, the playlist references itself or another playlist which
     * (eventually) references this one
     */
    g_printerr (_("recursion in playlist: %s\n"), mrl);
    return 0;
  }

  queued = g_slist_prepend (queued, (char *)mrl);
  ret = playlist_add_mrl_internal (mrl, ins_pos);
  queued = g_slist_remove (queued, mrl);

  return ret;
}

void playlist_play (int list_pos)
{
  play_item_t *item = playlist_get_item (list_pos);
  if (!item)
  {
    playlist_logo (NULL);
    return;
  }

  playlist_play_from (list_pos, 0, item->start_time);
}

static void set_mark_play_item (gboolean state)
{
  if (cur_list_pos >= 0 && cur_list_pos < playlist_size ())
  {
    GtkTreeIter iter;
    gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (pl_store), &iter, NULL,
				   cur_list_pos);
    gtk_list_store_set (pl_store, &iter, COLUMN_MARKING, state ? "•" : "", -1);
  }
}

void playlist_logo (gpointer wait_if_non_null)
{
  unmark_play_item ();
  cur_item_dispose ();
  logo_mode = 1;
  player_launch (NULL, logo_mrl, 0, 0);
  if (wait_if_non_null)
    player_wait ();
}

void playlist_show (void)
{
  if (is_visible)
  {
    is_visible = FALSE;
    gtk_widget_hide (dlg);
  }
  else
  {
    is_visible = TRUE;
    window_show (dlg, NULL);
    gtk_tree_view_columns_autosize (tree_view);
  }
}

int playlist_showing_logo (void)
{
  return logo_mode;
}

static gboolean row_activated_lcb (GtkWidget *widget, GtkTreePath *path,
				   GtkTreeViewColumn *col, gpointer data)
{
  GtkTreeIter iter;

  if (gtk_tree_selection_get_selected (sel, NULL, &iter))
    playlist_play (gtk_tree_path_get_indices (path)[0]);

  return FALSE;
}

static int has_been_played (int pos)
{
  return playlist_get_item (pos)->played;
}

static void play_next (void)
{
  int is_repeat, is_random;

  is_repeat = gtk_toggle_button_get_active (repeat_button);
  is_random = gtk_toggle_button_get_active (random_button);

  unmark_play_item ();
  ref_list_pos = -1;

  if (is_random)
  {
    cur_list_pos = rand() % playlist_size();
    int count = 0;
    while ((count<playlist_size()) && has_been_played (cur_list_pos))
    {
      cur_list_pos = (cur_list_pos + 1) % playlist_size();
      count++;
    }

    if (has_been_played (cur_list_pos))
      cur_list_pos = playlist_size()+1;
  } else
    cur_list_pos++;

  if (is_repeat && (cur_list_pos>=playlist_size()))
  {
    if (is_random)
    {
      int i;
      /* reset all played entries */
      for (i=0; i<playlist_size(); i++)
      {
	play_item_t *item;
	item = playlist_get_item (i);
	item->played = 0;
      }

      cur_list_pos = rand() % playlist_size();
    } else
      cur_list_pos = 0;
  }

  if (cur_list_pos<playlist_size())
  {
    play_item_t *item;
    item = playlist_get_item (cur_list_pos);
    item->played = 1;
    mark_play_item ();
    playlist_play (cur_list_pos);
  }
  else
  {
    cur_list_pos = 0;
    playlist_logo (NULL);
  }
}

static void xine_event_cb (void *user_data, const xine_event_t *event)
{
  /*
  g_print ("playlist: got an event %d\n", event->type);
  */
  static gboolean mrl_ext = FALSE;

  switch (event->type)
  {
  case XINE_EVENT_UI_PLAYBACK_FINISHED:
    gdk_threads_enter();
    ui_set_control_adjustment (Control_SEEKER, 0);
    if (!logo_mode)
    {
      se_eval (gse, "eval (event.stream_end);", NULL, NULL, NULL, "event.stream_end");
      play_next ();
    }
    gdk_threads_leave();
    break;

  case XINE_EVENT_PROGRESS:
    {
      xine_progress_data_t *prg = event->data;

      gdk_threads_enter();
      if (prg->percent>99)
	infobar_show_metadata (infobars);
      else
      {
	struct timeval  tv;
	int             age;

	/* filter out old events */
	gettimeofday (&tv, NULL);
	age = abs (tv.tv_sec - event->tv.tv_sec);
	if (age > 0)
	{
	  gdk_threads_leave();
	  break;
	}

	gxineinfo_update_line (infobars, 0, "%s %d%%", prg->description,
			       prg->percent);
      }
      gdk_flush();
      gdk_threads_leave();
    }
    break;

  case XINE_EVENT_UI_SET_TITLE:
  case XINE_EVENT_FRAME_FORMAT_CHANGE: /* in case of DVB etc. */
    gdk_threads_enter();
    infobar_show_metadata (infobars);
    playlist_check_set_title ();
    gdk_threads_leave();
    break;

#ifndef XINE_EVENT_MRL_REFERENCE_EXT
/* present in 1.1.0 but not 1.0.2 */
#define XINE_EVENT_MRL_REFERENCE_EXT 13
typedef struct {
  int alternative, start_time, duration;
  const char mrl[1];
} xine_mrl_reference_data_ext_t;
#endif
  case XINE_EVENT_MRL_REFERENCE_EXT:
    {
      xine_mrl_reference_data_ext_t *ref = event->data;
      const char *title = ref->mrl + strlen (ref->mrl) + 1;

      mrl_ext = TRUE;

      logprintf ("playlist: got a reference event (ref MRL: %s) (title: %s)\n",
		 ref->mrl, title);

      if (ref->alternative == 0)
      {
	play_item_t *item = play_item_new (*title ? title : NULL, ref->mrl,
					   ref->start_time);
	item->type = PLAY_ITEM_PLAYLIST;
	gdk_threads_enter ();

	if (ref_list_pos == -1)
	{
	  playlist_flush (PLAY_ITEM_PLAYLIST);
	  ref_list_pos = cur_list_pos + 1;
	  playlist_add (item, ref_list_pos);
	  /* remove the item which triggered this reference event */
	  /*
	  GtkTreeIter iter;
	  gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (pl_store), &iter,
					 NULL, cur_list_pos--);
	  gtk_list_store_remove (pl_store, &iter);
	  */
	}
	else
	  playlist_add (item, ++ref_list_pos);

	gdk_threads_leave ();
      }
    }
    break;
  case XINE_EVENT_MRL_REFERENCE:
    if (!mrl_ext)
    {
      xine_mrl_reference_data_t *ref = (xine_mrl_reference_data_t *) event->data;

      logprintf ("playlist: got a reference event (ref MRL: %s)\n", ref->mrl);

      if (ref->alternative == 0)
      {
	play_item_t *item = play_item_new (NULL, ref->mrl, 0);
	item->type = PLAY_ITEM_PLAYLIST;
	gdk_threads_enter ();

	if (ref_list_pos == -1)
	{
	  playlist_flush (PLAY_ITEM_PLAYLIST);
	  ref_list_pos = cur_list_pos + 1;
	  playlist_add (item, ref_list_pos);
	  /* remove the item which triggered this reference event */
	  /*
	  GtkTreeIter iter;
	  gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (pl_store), &iter,
					 NULL, cur_list_pos--);
	  gtk_list_store_remove (pl_store, &iter);
	  */
	}
	else
	  playlist_add (item, ++ref_list_pos);

	gdk_threads_leave ();
      }
    }
    break;

  case XINE_EVENT_UI_MESSAGE:
    {
      xine_ui_message_data_t *data = (xine_ui_message_data_t *) event->data;
      typeof (display_info) *msg;
      const char *fallback = NULL;

      gdk_threads_enter();

      switch (data->type)
      {
      default:
        g_print (_("xine_event_cb: unknown XINE_MSG_*"));
      case XINE_MSG_NO_ERROR:
      case XINE_MSG_AUDIO_OUT_UNAVAILABLE:
      case XINE_MSG_ENCRYPTED_SOURCE:
        msg = display_info;
        break;

      case 13 /* UNCOMMENTME: XINE_MSG_FILE_EMPTY */:
        fallback = _("File is empty:"); /* missing in xine-lib < 1.1.2 */
      case XINE_MSG_GENERAL_WARNING:
      case XINE_MSG_NETWORK_UNREACHABLE:
      case XINE_MSG_UNKNOWN_HOST:
        msg = display_warning;
        break;

      case XINE_MSG_READ_ERROR:
      case XINE_MSG_LIBRARY_LOAD_ERROR:

      case XINE_MSG_CONNECTION_REFUSED:
      case XINE_MSG_FILE_NOT_FOUND:
      case XINE_MSG_PERMISSION_ERROR:
      case XINE_MSG_SECURITY:
      case XINE_MSG_UNKNOWN_DEVICE:
        msg = display_error;
        break;
      }

      /* TODO: provide customized messages, hints... */
      if (data->num_parameters)
      {
        int i;
	const char *param = (char *)data + data->parameters;
	char *txt = strdup ("");
	for (i = 0; i < data->num_parameters; ++i)
	{
	  asreprintf (&txt, "%s %s", txt, param);
	  param += strlen (param) + 1;
	}
	msg (FROM_XINE,
	     data->explanation ? (char *)data + data->explanation
			       : fallback ? : NULL,
	     "%s", txt);
	free (txt);
      }
      else if (data->explanation)
	msg (FROM_XINE, (char *)data + data->explanation, "");
      else if (fallback)
	msg (FROM_XINE, fallback, "");
      else
	msg (FROM_XINE, NULL, _("Message code %d"), data->type);

      gdk_threads_leave();
    }
    break;

  case XINE_EVENT_AUDIO_LEVEL:
    {
      xine_audio_level_data_t *data = event->data;
      gdk_threads_enter ();
      ui_set_control_adjustment (Control_VOLUME,
				 (data->left + data->right) / 2);
      ui_set_control_button (Control_MUTE, !data->mute);
      gdk_threads_leave ();
    }
    break;
  }
}

void playlist_check_set_title (void)
{
  const char *title;
  if ((title = xine_get_meta_info (stream, XINE_META_INFO_TITLE)))
  {
    GtkTreeIter iter;

    gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (pl_store), &iter,
				   NULL, cur_list_pos);
    play_item_t *play_item = peek_play_item (&iter);

    if (play_item->untitled)
    {
      free (play_item->title);
      play_item->title = strdup (title);
      play_item->untitled = FALSE;
      gtk_list_store_set (pl_store, &iter, 0, play_item->title, -1);
    }

    CUR_ITEM_LOCK ();
    if (cur_item && cur_item->untitled)
    {
      free (cur_item->title);
      cur_item->title = strdup (title);
      cur_item->untitled = FALSE;
    }
    CUR_ITEM_UNLOCK ();
  }
}

/*
 * drag and drop
 * FIXME: should subclass GtkListStore
 */

static void drop_cb (GtkTreeView	*widget,
		     GdkDragContext     *context,
		     gint                x,
		     gint                y,
		     GtkSelectionData   *data,
		     guint               info,
		     guint		 time)
{
  gint ins_pos = treeview_get_drop_index (widget, context, x, y);

  logprintf ("drag_drop: drop callback, length=%d, format=%d, pos=%d,%d, insert=%d\n",
	     data->length, data->format, x, y, ins_pos);

  if (data->format == 8)
  {
    /* Drag is from an external source */
    dnd_add_mrls (data, &cur_list_pos, ins_pos);
    gtk_drag_finish (context, TRUE, FALSE, time);
    return;
  }

  if (drag_mrl.index == -1)
  {
    /* below here, we're only interested in intra-app drags */
    gtk_drag_finish (context, FALSE, FALSE, time);
    return;
  }

  if (drag_mrl.model == (GtkTreeModel *)pl_store)
  {
    /* Moving a playlist item */
    GtkTreeIter from, to;

    if (drag_mrl.index == ins_pos)
    {
      /* no need to move the item */
      gtk_drag_finish (context, TRUE, FALSE, time);
      return;
    }

    gtk_tree_model_iter_nth_child (drag_mrl.model, &from, NULL, drag_mrl.index);
    if (ins_pos == -1)
    {
      gtk_list_store_move_before (pl_store, &from, NULL);
      ins_pos = gtk_tree_model_iter_n_children (drag_mrl.model, NULL);
    }
    else
    {
      gtk_tree_model_iter_nth_child (drag_mrl.model, &to, NULL, ins_pos);
      gtk_list_store_move_before (pl_store, &from, &to);
    }

    /* adjust index of currently-being-played item if necessary */
    if (cur_list_pos != -1)
    {
      if (cur_list_pos == drag_mrl.index)
	cur_list_pos = ins_pos;
      if (drag_mrl.index < ins_pos && drag_mrl.index <= cur_list_pos
	  && cur_list_pos <= ins_pos)
	--cur_list_pos;
      if (drag_mrl.index > ins_pos && drag_mrl.index >= cur_list_pos
	  && cur_list_pos >= ins_pos)
	++cur_list_pos;
    }

    gtk_drag_finish (context, TRUE, FALSE, time);
    return;
  }

  gtk_drag_finish (context, FALSE, FALSE, time);
}

/*
 * js functions
 */

static JSBool js_playlist_get_item (JSContext *cx, JSObject *obj, uintN argc,
				    jsval *argv, jsval *rval)
{
  /* se_t *se = (se_t *) JS_GetContextPrivate(cx); */
  se_log_fncall ("playlist_get_item");
  *rval = INT_TO_JSVAL (playlist_get_list_pos());
  return JS_TRUE;
}

static JSBool js_playlist_clear (JSContext *cx, JSObject *obj, uintN argc,
				 jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("playlist_clear");
  playlist_clear ();
  *rval = JSVAL_VOID;
  return JS_TRUE;
}

static JSBool js_playlist_flush (JSContext *cx, JSObject *obj, uintN argc,
				 jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("playlist_flush");
  playlist_flush (PLAY_ITEM_NORMAL);
  *rval = JSVAL_VOID;
  return JS_TRUE;
}

static JSBool js_playlist_load (JSContext *cx, JSObject *obj, uintN argc,
				jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("playlist_load");

  se_argc_check (1, "playlist_load");
  se_arg_is_string_or_null (0, "playlist_load");

  char *mrl = JS_GetStringBytes (JS_ValueToString (cx, argv[0]));

  logprintf ("playlist_load: file=%s\n", mrl);
  playlist_load_any (mrl);

  *rval = JSVAL_VOID;
  return JS_TRUE;
}

static JSBool js_playlist_add (JSContext *cx, JSObject *obj, uintN argc,
			       jsval *argv, jsval *rval)
{

  se_log_fncall_checkinit ("playlist_add");

  se_argc_check_range (1, 2, "playlist_add");
  se_arg_is_string (0, "playlist_add");

  char *mrl = JS_GetStringBytes (JS_ValueToString (cx, argv[0]));
  int item;

  if (argc > 1)
  {
    se_arg_is_string_or_null (1, "playlist_add");
    char *title = JSVAL_IS_STRING (argv[1])
		  ? JS_GetStringBytes (JS_ValueToString (cx, argv[1]))
		  : NULL;
    logprintf ("playlist_add: MRL=%s title=%s\n", mrl, title);
    play_item_t *play_item = play_item_new (title, mrl, 0);
    item = playlist_add (play_item, -1);
  }
  else
  {
    logprintf ("playlist_add: MRL=%s\n", mrl);
    item = playlist_add_mrl (mrl, -1);
  }

  *rval = INT_TO_JSVAL (item);

  return JS_TRUE;
}

static JSBool js_playlist_delete (JSContext *cx, JSObject *obj, uintN argc,
				  jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("playlist_remove");

  se_argc_check_range (1, 2, "playlist_remove");
  se_arg_is_int (0, "playlist_remove");

  int32 pos;
  JS_ValueToInt32 (cx, argv[0], &pos);

  int count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (pl_store), NULL);
  if (pos < 0)
    pos = count - pos;
  if (pos >= 0 && pos < count)
  {
    GtkTreeIter iter;
    gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (pl_store), &iter, NULL, pos);

    if (pos == cur_list_pos && !logo_mode)
      play_next ();

    if (pos <= cur_list_pos)
      --cur_list_pos;
    gtk_list_store_remove (pl_store, &iter);
  }

  *rval = JSVAL_VOID;
  return JS_TRUE;
}

static JSBool js_playlist_play (JSContext *cx, JSObject *obj, uintN argc,
				jsval *argv, jsval *rval)
{
  int32 item;

  se_log_fncall_checkinit ("playlist_play");

  se_argc_check (1, "playlist_play");
  se_arg_is_int (0, "playlist_play");

  JS_ValueToInt32 (cx, argv[0], &item);

  playlist_play (item);

  *rval = JSVAL_VOID;
  return JS_TRUE;
}

static JSBool js_playlist_show (JSContext *cx, JSObject *obj, uintN argc,
				jsval *argv, jsval *rval)
{
  se_log_fncall_checkinit ("playlist_show");
  playlist_show ();
  *rval = JSVAL_VOID;
  return JS_TRUE;
}

static JSBool js_mrl_browser_refresh (JSContext *cx, JSObject *obj, uintN argc,
				      jsval *argv, jsval *rval)
{
  GtkTreeIter iter;
  se_log_fncall_checkinit ("mrl_browser_refresh");

  if (cur_list_pos < 0
      || !gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (pl_store), &iter, NULL,
					 cur_list_pos)
      || !item_marked_current (&iter))
  {
    playlist_flush (PLAY_ITEM_BROWSER);
    *rval = JSVAL_VOID;
    return JS_TRUE;
  }

  CUR_ITEM_LOCK ();
  playlist_browse_set (cur_item ? : playlist_get_item (cur_list_pos));
  CUR_ITEM_UNLOCK ();
  *rval = JSVAL_VOID;
  return JS_TRUE;
}

void playlist_play_from (int list_pos, int pos, int pos_time)
{
  /* int          err; */
  GtkTreePath *path;
  char         indices[10];
  play_item_t *play_item;
  static gint  browse_cb = 0;

  logo_mode = 0;

  unmark_play_item ();
  cur_item_dispose ();

  list_pos = playlist_clip (list_pos);
  play_item = playlist_get_item (list_pos);

  if (!play_item)
  {
    cur_list_pos = 0;
    playlist_logo (NULL);
    return;
  }

  play_item_play (play_item, pos, pos_time);

  if (browse_cb)
    g_source_remove (browse_cb);
  browse_cb = g_timeout_add (1000, (GSourceFunc)playlist_browse_set, play_item);

  snprintf (indices, sizeof (indices), "%d", list_pos);
  path = gtk_tree_path_new_from_string (indices);
  gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE);
  gtk_tree_path_free (path);

  cur_list_pos = list_pos;
  ref_list_pos = -1;

  mark_play_item ();
}

static const GtkActionEntry buttons_data[] = {
  { "open",	GTK_STOCK_OPEN,		NULL,			NULL,		N_("Open a playlist file"),			open_cb },
  { "saveas",	GTK_STOCK_SAVE_AS,	NULL,			NULL,		N_("Save the playlist file"),			save_as_cb },
  { "clear",	GTK_STOCK_CLEAR,	NULL,			NULL,		N_("Clear the playlist"),			clear_cb },
  { "fromfile",	GTK_STOCK_OPEN,		NULL,			NULL,		N_("Add files to the playlist"),		add_cb },
  { "new",	GTK_STOCK_NEW,		NULL,			NULL,		NULL /* FIXME: post-0.5.5 */,			new_cb },
  { "edit",	GTK_STOCK_EDIT,		NULL,			"<Control>E",	N_("Edit the selected item"),			edit_cb },
  { "delete",	GTK_STOCK_DELETE,	NULL,			"Delete",	N_("Delete the selected item"),			del_cb },
  { "copy",	GTK_STOCK_COPY,		NULL,			NULL,		NULL,						copy_cb },
  { "cut",	GTK_STOCK_CUT,		NULL,			NULL,		NULL,						cut_cb },
  { "paste",	GTK_STOCK_PASTE,	NULL,			NULL,		NULL,						paste_cb },
  { "mm",	GXINE_MEDIA_MARK,	N_("Add to _media marks"), "<Control>M",N_("Add the selected item to the media marks list"), make_mediamark_cb },
  { "prev",	GTK_STOCK_MEDIA_PREVIOUS, NULL,			NULL,		N_("Play previous item"),			playlist_prev_cb },
  { "next",	GTK_STOCK_MEDIA_NEXT,	NULL,			NULL,		N_("Play next item"),				playlist_next_cb },
};

static const char buttons_structure[] =
  "<ui>\n"
    "<toolbar>\n"
      "<toolitem action='open' />\n"
      "<toolitem action='saveas' />\n"
      "<toolitem action='clear' />\n"
      "<separator />\n"
      "<toolitem action='fromfile' />\n"
      "<toolitem action='new' />\n"
      "<toolitem action='edit' />\n"
      "<toolitem action='delete' />\n"
      "<toolitem action='copy' />\n"
      "<toolitem action='cut' />\n"
      "<toolitem action='paste' />\n"
      "<toolitem action='mm' />\n"
      "<separator />\n"
      "<toolitem action='prev' />\n"
      "<toolitem action='next' />\n"
    "</toolbar>\n"
  "</ui>";

static void sel_changed_cb (GtkTreeSelection *sel, gpointer data)
{
  static const char *const edit_items[] = {
    "edit", NULL
  };
  static const char *const move_items[] = {
    "delete", "mm", "copy", "cut", NULL
  };
  GtkTreeIter iter;
  if (gtk_tree_selection_get_selected (sel, NULL, &iter))
  {
    ui_mark_active (pl_ui, edit_items, item_is_normal (&iter));
    ui_mark_active (pl_ui, move_items, TRUE);
  }
  else
  {
    ui_mark_active (pl_ui, edit_items, FALSE);
    ui_mark_active (pl_ui, move_items, FALSE);
  }
}

static void check_list_empty (void)
{
  static const char *const items[] = {
    "saveas", "clear", "prev", "next", NULL
  };
  ui_mark_active (pl_ui, items,
		  gtk_tree_model_iter_n_children (GTK_TREE_MODEL (pl_store),
						  NULL));
  ui_set_status (UI_CURRENT_STATE);
}

static void get_playlist_repeat (se_prop_t *prop, se_prop_read_t *value)
{
  value->i = gtk_toggle_button_get_active (repeat_button);
}

static void get_playlist_random (se_prop_t *prop, se_prop_read_t *value)
{
  value->i = gtk_toggle_button_get_active (random_button);
}

static void set_playlist_repeat (int v)
{
  gtk_toggle_button_set_active (repeat_button, v);
}

static void set_playlist_random (int v)
{
  gtk_toggle_button_set_active (random_button, v);
}

void playlist_init (void)
{
  GtkWidget *hbox, *scrolled_window;
  GtkBox *vbox;
  GtkCellRenderer      *cell;
  GtkTreeViewColumn    *column;
  GError *error = NULL;

  is_visible     = 0;
  cur_list_pos   = -3; /* this will be clipped to 0 when Play is pressed */

  srand (time (NULL));

  /*
   * window
   */

  dlg = gtk_dialog_new_with_buttons (_("Edit playlist..."), NULL, 0,
				     GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
				     NULL);
  gtk_window_set_default_size (GTK_WINDOW (dlg), 500, 400);
  g_object_connect (G_OBJECT (dlg),
	"signal::delete-event", G_CALLBACK (close_cb), NULL,
	"signal::response", G_CALLBACK(close_cb), NULL,
	NULL);

  vbox = GTK_BOX (GTK_DIALOG (dlg)->vbox);

  /*
   * init tree store & view
   */

  pl_store = gtk_list_store_new (5, G_TYPE_STRING, G_TYPE_STRING,
				 G_TYPE_POINTER, G_TYPE_STRING, G_TYPE_STRING);
  g_object_connect (G_OBJECT (pl_store),
	"signal::row-deleted", G_CALLBACK (check_list_empty), NULL,
	"signal::row-inserted", G_CALLBACK (check_list_empty), NULL,
	NULL);

  tree_view = GTK_TREE_VIEW (gtk_tree_view_new_with_model (GTK_TREE_MODEL (pl_store)));
  gtk_tree_view_set_rules_hint (tree_view, TRUE);
  g_signal_connect (G_OBJECT(tree_view), "row-activated",
		    G_CALLBACK(row_activated_lcb), NULL);

  sel = gtk_tree_view_get_selection (tree_view);
  g_signal_connect (G_OBJECT (sel), "changed",
		    G_CALLBACK (sel_changed_cb), NULL);

  /*
   * buttons
   */

  pl_ui = ui_create_manager ("file", dlg);
  gtk_action_group_add_actions (ui_get_action_group (pl_ui), buttons_data,
				G_N_ELEMENTS (buttons_data), (GtkWidget *)tree_view);
  gtk_ui_manager_add_ui_from_string (pl_ui, buttons_structure, -1, &error);
  if (error)
  {
    g_printerr (_("playlist XML: %s\n"), error->message);
    g_clear_error (&error);
  }
  gtk_box_pack_start (vbox, gtk_ui_manager_get_widget (pl_ui, "/toolbar"),
		      FALSE, FALSE, 0);
  gtk_action_group_connect_accelerators (ui_get_action_group (pl_ui));

  sel_changed_cb (sel, NULL);

  /*
   * install tree view
   */

  cell = gtk_cell_renderer_text_new ();
  column = gtk_tree_view_column_new_with_attributes (" ", cell, "text",
						     COLUMN_MARKING, NULL);
  gtk_tree_view_append_column (tree_view, column);

  cell = gtk_cell_renderer_pixbuf_new ();
  column = gtk_tree_view_column_new_with_attributes (" ", cell, "stock-id",
						     COLUMN_SOURCE, NULL);
  gtk_tree_view_append_column (tree_view, column);
  g_object_set (cell, "stock-size", 1, NULL);

  cell = gtk_cell_renderer_text_new ();
  g_object_set (G_OBJECT(cell),
		"ellipsize", 2, "width_chars", 16, NULL); /* foo...bar */
  column = gtk_tree_view_column_new_with_attributes (_("Title"), cell, "text",
						     COLUMN_TITLE, NULL);
  gtk_tree_view_column_set_resizable (column, TRUE);
  gtk_tree_view_append_column (tree_view, column);

  cell = gtk_cell_renderer_text_new ();
  g_object_set (G_OBJECT(cell),
		"ellipsize", 1, "width_chars", 16, NULL); /* ...bar */
  column = gtk_tree_view_column_new_with_attributes (_("MRL"), cell, "text",
						     COLUMN_MRL, NULL);
  gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
  gtk_tree_view_append_column (tree_view, column);

  treeview_drag_drop_setup (tree_view, drop_cb);

  gtk_widget_show ((GtkWidget *)tree_view);

  scrolled_window = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
				  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  gtk_container_add (GTK_CONTAINER (scrolled_window), (GtkWidget *)tree_view);

  gtk_box_pack_start (vbox, scrolled_window, TRUE, TRUE, 2);

  /*
   * checkboxes (repeat, random)
   */

  hbox = gtk_hbox_new (0,2);
  gtk_box_pack_start (vbox, hbox, FALSE, FALSE, 2);

  repeat_button = (GtkToggleButton *)gtk_check_button_new_with_mnemonic (_("_Repeat"));
  gtk_box_pack_start (GTK_BOX (hbox), repeat_button, FALSE, FALSE, 2);

  random_button = (GtkToggleButton *)gtk_check_button_new_with_mnemonic (_("_Random"));
  gtk_box_pack_start (GTK_BOX (hbox), random_button, FALSE, FALSE, 2);

  sprintf (logo_mrl, "%s/logo."LOGO_FORMAT, logodir);
  logo_mode = 1;

  playlist_load (NULL);
  check_list_empty ();

  /*
   * event handling
   */
  xine_event_create_listener_thread (xine_event_new_queue (stream),
				     xine_event_cb, NULL);

  {
    static const se_f_def_t defs[] = {
      { "playlist_show", js_playlist_show, 0, 0,
	SE_GROUP_DIALOGUE, NULL, NULL },
      { "playlist_clear", js_playlist_clear, 0, 0,
	SE_GROUP_PLAYLIST, NULL, NULL },
      { "playlist_flush", js_playlist_flush, 0, 0,
	SE_GROUP_PLAYLIST, NULL, NULL },
      { "playlist_load", js_playlist_load, 0, 0,
	SE_GROUP_PLAYLIST, N_("file"), NULL },
      { "playlist_add", js_playlist_add, 0, 0,
	SE_GROUP_PLAYLIST, N_("MRL[, title]"), NULL },
      { "playlist_delete", js_playlist_delete, 0, 0,
	SE_GROUP_PLAYLIST, N_("int"), NULL },
      { "playlist_play", js_playlist_play, 0, 0,
	SE_GROUP_PLAYLIST, N_("int"), N_("playlist entry number") },
      { "playlist_get_item", js_playlist_get_item, 0, 0,
	SE_GROUP_PLAYLIST, NULL, NULL },
      { "mrl_browser_refresh", js_mrl_browser_refresh, 0, 0,
	SE_GROUP_HIDDEN, NULL, NULL },
      { NULL }
    };
    static const ui_property_t props[] = {
      { "repeat", N_("v=bool, toggle()"), 0, get_playlist_repeat, set_playlist_repeat },
      { "random", N_("v=bool, toggle()"), 0, get_playlist_random, set_playlist_random },
      { NULL }
    };
    se_o_t *playlist = se_create_object (gse, NULL, "playlist", NULL,
		       SE_GROUP_PROPERTIES, NULL);
    ui_create_properties (props, playlist, SE_TYPE_BOOL);
    se_defuns (gse, NULL, defs);
  }
}
